﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Reflection;
using Microsoft.Office.Interop.PowerPoint;
using System.Windows.Forms;
using Microsoft.Office.Core;
using ActiproSoftware;
using ActiproSoftware.CodeHighlighter;
using ActiproSoftware.SyntaxEditor.Addons.Dynamic;

namespace WildRiley.PPTFormatCodeAddin
{
    class CodeFormatter
    {
        /// <summary>
        /// List of supported languages - each should have a dynamic language file
        /// of the form ActiproSoftware.LANGUAGE.xml.  Removing any language from this
        /// enum will ensure it is not available for use in the Addin.
        /// </summary>
        public enum Language
        {
            Assembly = 0,
            BatchFile,
            CSharp,
            CSS,
            HTML,
            INIFile,
            Java,
            JScript,
            Lua,
            MSIL,
            Pascal,
            Perl,
            PHP,
            Powershell,
            Python,
            SQL,
            VBDotNet,
            VBScript,
            XAML,
            XML,
            LanguageMax     // End of languages
        }

        /// <summary>
        /// Constants used for code formatting (should really be in app.config and customizable)
        /// </summary>
        private class Constants
        {
            public const string CodeFontName = "Consolas";
            public const byte TabsInSpaces = 4;
        }

        /// <summary>
        /// Main entry point for code formatting.  Tries to find a selected text object with non-zero
        /// length and will format it in the specified language spec.
        /// </summary>
        /// <param name="lang">Syntax for target selection</param>
        public static void FormatCodeSelection(Language lang)
        {
            try
            {
                if (Globals.ThisAddIn.Application.Windows.Count > 0)
                {
                    // Find a text range if possible
                    //
                    DocumentWindow window = Globals.ThisAddIn.Application.Windows[1];
                    TextRange tr = null;

                    switch (window.Selection.Type)
                    {
                        // Shape with text content
                        case PpSelectionType.ppSelectionShapes:
                            if (window.Selection.ShapeRange.TextFrame.HasText == MsoTriState.msoTrue)
                                tr = window.Selection.ShapeRange.TextFrame.TextRange;
                            break;

                        // Explicit text selection (with cursor)
                        case PpSelectionType.ppSelectionText:
                            tr = window.Selection.TextRange;
                            break;

                        // Slide
                        case PpSelectionType.ppSelectionSlides:
                            Microsoft.Office.Interop.PowerPoint.Shapes shapes = window.Selection.SlideRange.Shapes;
                            foreach (Microsoft.Office.Interop.PowerPoint.Shape shape in shapes)
                            {
                                // Skip title shape
                                if (shapes.HasTitle == MsoTriState.msoTrue && shape == shapes.Title)
                                {
                                    continue;
                                }
                                // If object has text frame with content, select it
                                else if (shape.HasTextFrame == MsoTriState.msoTrue && 
                                    shape.TextFrame.HasText == MsoTriState.msoTrue)
                                {
                                    tr = shape.TextFrame.TextRange;
                                    break;
                                }
                            }
                            break;
                    }
                    
                    // Format text range if found
                    //
                    if (tr != null && tr.Length > 0)
                    {
                        // Save off font size
                        float fontSize = tr.Font.Size;

                        // Format selected text
                        string formatText = FormatCodeAsHtml(tr.Text, lang);

                        // Preserve current clipboard data and then push current format text onto
                        // clipboard, pasting it into the selected text range in CF_HTML format
                        //
                        IDataObject clipData = Clipboard.GetDataObject();
                        Clipboard.Clear();
                        Clipboard.SetText(HtmlToClipboardHtml(formatText), TextDataFormat.Html);
                        tr.PasteSpecial(PpPasteDataType.ppPasteHTML, MsoTriState.msoFalse, "", 0, "", MsoTriState.msoFalse);

                        // Set target font and restore size
                        tr.Font.Name = Constants.CodeFontName;
                        tr.Font.Size = fontSize;

                        // Restore clipboard
                        Clipboard.SetDataObject(clipData);

                        return;
                    }
                    else
                    {
                        MessageBox.Show("No text was selected", "Selection Error");
                    }
                }
            }
            catch (ArgumentException ae)
            {
                MessageBox.Show(ae.Message);
                return;
            }
        }

        /// <summary>
        /// Converts supplied HTML text into Clipboard HTML format
        /// See http://support.microsoft.com/kb/274308
        /// </summary>
        /// <param name="formatText">HTML input text</param>
        /// <returns>CF_HTML</returns>
        private static string HtmlToClipboardHtml(string formatText)
        {
            const string htmlTextBegin = "Version:0.9\r\n" +
                                "StartHTML:-1\r\n" +
                                "EndHTML:-1\r\n" +
                                "StartFragment: SSSSSSSS\r\n" +
                                "EndFragment: EEEEEEEE\r\n" +
                                "<html><body>\r\n" +
                                "<!--StartFragment -->\r\n";
            const string htmlTextEnd = "<!--EndFragment -->\r\n</body></html>";

            int beginFragment = htmlTextBegin.Length;
            int endFragment = beginFragment + formatText.Length;

            string cfHtmlText = htmlTextBegin.Replace("SSSSSSSS", String.Format("{0:00000000}", beginFragment));
            cfHtmlText = cfHtmlText.Replace("EEEEEEEE", String.Format("{0:00000000}", endFragment));
            cfHtmlText += formatText;
            cfHtmlText += htmlTextEnd;

            return cfHtmlText;
        }

        /// <summary>
        /// Formats the supplied text in the target language syntax
        /// </summary>
        /// <param name="text">Code text</param>
        /// <param name="lang">Syntax language</param>
        /// <returns>HTML output in syntax format</returns>
        private static string FormatCodeAsHtml(string text, Language lang)
        {
            // Try for files on local disk first
            //
            //Uri assemblyUri = new Uri(Assembly.GetExecutingAssembly().CodeBase);
            //FileInfo thisAddin = new FileInfo(assemblyUri.LocalPath);
            //string commonFilesDir = thisAddin.DirectoryName;

            try
            {
                using (Stream langStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(String.Format("PPTPasteAsCodeAddin.LanguageResources.ActiproSoftware.{0}.xml", lang)))
                using (ActiproSoftware.SyntaxEditor.SyntaxLanguage language = DynamicSyntaxLanguage.LoadFromXml(langStream, 0))
                {
                    CodeHighlighterEngine engine = new CodeHighlighterEngine();
                    engine.SpacesInTabs = Constants.TabsInSpaces;
                    engine.OutliningEnabled = false;

                    // Normalize input text linefeeds to ensure they are preserved
                    string inputText = text.Replace("\r", "\n");

                    // Call library to syntax format input text in target language
                    string htmlText = engine.GenerateHtmlInline("", inputText, language);

                    // Fixup leading whitespace on linebreak boundaries - this ensures the original indentation
                    // and formatting is preserved.  This limitation comes from the Actipro software library, currently
                    // they have no support for automatic layout formatting.
                    //
                    StringBuilder sb = new StringBuilder(htmlText.Length);
                    foreach (string line in htmlText.Split(new char[] { '\n' }))
                    {
                        // Find spacing prefix on line
                        int c;
                        for (c = 0; c < line.Length; c++)
                        {
                            // End of whitespace sequence?
                            if (line[c] != ' ')
                            {
                                break;
                            }

                            sb.Append("&nbsp;");
                        }

                        // Append remainder of line and force HTML break
                        sb.Append(line.Substring(c));
                        sb.AppendLine("<br>");
                    }

                    return sb.ToString();
                }
            }
            catch
            {
                throw new ArgumentException(String.Format("Unknown language format: {0}", lang));
            }
        }
    }
}
